package org.chipu.jnids.sancp;

import static org.chipu.jnids.sce.profile.StatType.Type.*;
import java.net.InetAddress;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.SwingWorker;
import org.chipu.jnids.beans.Flow;
import org.chipu.jnids.beans.Profile;
import org.chipu.jnids.controller.DBLayer;
import org.chipu.jnids.display.TextConsole;
import org.chipu.jnids.sce.profile.*;
import org.chipu.shared.controllers.ConfigController;
import org.chipu.shared.interfaces.Loggeable;
import org.chipu.shared.util.singletons.*;

/**
 * This class is in charge of collecting flow info during some time (from SANCP stats file) to create the
 * Internet profile, and store it in the database.
 */
public abstract class ProfileCreator implements Loggeable {
	protected Logger logger = Logger.getLogger(getClass().getName());
	protected final DBLayer dbController;
	protected final SancpManager sancp;
	private boolean collecting, proccessing, profiling;
	// -----
	/** In seconds */
	static final int timePrecision = ConfigController.INSTANCE
		.getInt("org.chipu.jnids.sancp.ProfileCreator.timePrecision");
	/** In seconds. By default is 20 min. */
	static final int timeBetweenStatistics = ConfigController.INSTANCE
		.getInt("org.chipu.jnids.sancp.ProfileCreator.timeBetweenStatistics");
	// ---- PERFORMANCE Tuning parameters (hash capacities (without considering the load factor))
	static final int snapshotConnectionSize = ConfigController.INSTANCE
		.getInt("org.chipu.jnids.sancp.ProfileCreator.snapshotConnectionSize");
	public static final int snapshotAddressSize = ConfigController.INSTANCE
		.getInt("org.chipu.jnids.sancp.ProfileCreator.snapshotAddressSize");
	public static final int snapshotActiveAddressSize = ConfigController.INSTANCE
		.getInt("org.chipu.jnids.sancp.ProfileCreator.snapshotActiveAddressSize");
	public static final int profileExternalAddressSize = ConfigController.INSTANCE
		.getInt("org.chipu.jnids.sancp.ProfileCreator.profileExternalAddressSize");
	public static final int profileInternalAddressSize = ConfigController.INSTANCE
		.getInt("org.chipu.jnids.sancp.ProfileCreator.profileInternalAddressSize");
	private transient SwingWorker<Void, PuntualStats> task;

	public ProfileCreator(DBLayer dbController, SancpManager sancp) {
		this.dbController = dbController;
		this.sancp = sancp;
	}

	@Override
	public void setLogger(Logger logger) {
		if (logger != null)
			this.logger = logger;
	}

	// -----Using database

	/**
	 * Creates the log file by running SANCP (if not already big enough).<br>
	 * Cannot run in the EDT
	 */
	public boolean collect() {
		if (isRunning())
			return false;
		collecting = true;
		if (!sancp.isSancpDataEnough()) {
			if (!sancp.isSancpRunning())
				sancp.startSancp();
			do
				try {
					Thread.sleep(100000);
				} catch(InterruptedException ex) {
					logger.log(Level.SEVERE, "", ex);
				}
			while(!sancp.isSancpDataEnough());
			sancp.stopSancp();
		}
		collecting = false;
		return true;
	}

	/**
	 * Process all the SANCP stat data, and store it into the database.
	 * @return If it was necessary, or a profile already existed
	 */
	public Profile createProfile(String name, boolean white) throws Exception {
		if (isRunning()) {
			logger.info("Profile creator is busy, cannot create the profile " + name);
			return null;
		}
		if (dbController.isThereAProfileStored()) {
			logger.info("There is already a profile called " + name);
			return null;
		}
		proccessing = true;
		Profile p = new Profile(name, white);
		process(p);

		logger.info("Profile created at " + System.currentTimeMillis() + "  " + task.getState());
		dbController.addProfile(p);
		proccessing = false;
		logger.info("Profile stored at " + System.currentTimeMillis());
		return p;
	}

	/** It blocks the thread */
	void process(final Profile p) throws Exception {
		if (task != null)
			task.cancel(true);
		task = new SwingWorker<Void, PuntualStats>() {
			private final String className = ProfileCreator.this.getClass().getSimpleName();

			// IMPROVE use multicore CPUs
			@Override
			public Void doInBackground() throws Exception {
				final int initTime = getInitTime();
				final int endTime = getEndTime();
				int nSnapshots = (endTime - initTime) / timePrecision;
				TextConsole.getInstance().startProgress(
					className,
					"Creating profile in " + ((endTime - initTime) / timeBetweenStatistics + 1)
						+ " statistics (" + nSnapshots + " snapshots, from " + Utils.toDate(initTime)
						+ " to " + Utils.toDate(endTime) + ")");
				if ((endTime - initTime) / timeBetweenStatistics == 0)
					throw new IllegalStateException("Not enough data to make a single statistic");
				ArrayList<Collection<Flow>> tmpIn = new ArrayList<Collection<Flow>>(nSnapshots);
				ArrayList<Collection<Flow>> tmpOut = new ArrayList<Collection<Flow>>(nSnapshots);

				long lastStatsTime = Utils.getInitOfDay(((long) initTime) * 1000);
				for(int time = initTime; time < endTime; time += timePrecision) {
					if (time > lastStatsTime / 1000 + timeBetweenStatistics) {
						lastStatsTime += ((long) timeBetweenStatistics) * 1000;
						// IMPROVE combine the snapshots
						PuntualStats ps = calculateStats(tmpIn, tmpOut, lastStatsTime);
						if (!ps.isEmpty())
							p.addStats(ps);
						publish(ps);
						tmpIn.clear();
						tmpOut.clear();
					}
					tmpIn.add(getConnectionsInAt(time));
					tmpOut.add(getConnectionsOutAt(time));
					// TODO update nconnections
					// IMPROVE return a snapshot
				}
				if (!tmpIn.isEmpty() || !tmpOut.isEmpty()) {
					PuntualStats ps = calculateStats(tmpIn, tmpOut, lastStatsTime
						+ ((long) timeBetweenStatistics) * 1000);
					if (!ps.isEmpty())
						p.addStats(ps);
					publish(ps);
				}
				return null;
			}

			// code here is in the EDT, so when the worker is finished, it may have not been executed
			// yet!!!
			@Override
			protected void process(List<PuntualStats> chunks) {
				for(int i = 0; i < chunks.size(); i++)
					TextConsole.getInstance().doProgress(className);
			}

			@Override
			protected void done() {
				try {
					TextConsole.getInstance().endProgress(className);
				} catch(Exception ex) {
					ex.printStackTrace();
				}
			}
		};
		task.execute();
		task.get();
	}

	protected abstract int getInitTime() throws Exception;

	protected abstract int getEndTime() throws Exception;

	protected abstract Collection<Flow> getConnectionsInAt(int time) throws Exception;

	protected abstract Collection<Flow> getConnectionsOutAt(int time) throws Exception;

	// -----

	/** It recreates the profile from the database to memory. */
	public Profile recreateProfile() {
		if (isRunning())
			return null;
		profiling = true;
		// TODOLATER create profile from the flow data stored in the DB
		profiling = false;
		return null;
	}

	// -----Processing files directly

	public boolean isRunning() {
		return collecting || proccessing || profiling;
	}

	public boolean cancelRunning() {
		if (!isRunning())
			return true;
		// TODOLATER
		return true;
	}

	// -----

	// private PuntualStats calculateStats(List<Set<Flow>> tmp, int initTimeInSeconds) {
	// IMPROVE: Simplify (performance cost?)
	// }

	/** Calculate the statistics for these connections, and store them */
	private PuntualStats calculateStats(List<Collection<Flow>> tmpIn, List<Collection<Flow>> tmpOut,
		long initTimeUTC) {
		int[] nConnectionsIn = new int[tmpIn.size()];
		int[] nConnectionsOut = new int[tmpOut.size()];
		// using network protocols
		int c = (int) (NetworkUtils.getKnownNetworkProtocolsCount() / 0.75) + 1;
		Map<Byte, int[]> nConnectionsInByProt = new HashMap<Byte, int[]>(c);
		Map<Byte, int[]> nConnectionsOutByProt = new HashMap<Byte, int[]>(c);
		for(Byte b : NetworkUtils.getKnownNetworkProtocols()) {
			nConnectionsInByProt.put(b, new int[tmpIn.size()]);
			nConnectionsOutByProt.put(b, new int[tmpOut.size()]);
		}
		// using hosts
		/** Connections into out network from external hosts */
		// Map<InetAddress, int[]> nConnectionsInByExternalHost = new HashMap<InetAddress, int[]>(
		// ProfileCreator.profileExternalAddressSize);
		// Map<InetAddress, int[]> nConnectionsOutByExternalHost = new HashMap<InetAddress, int[]>(
		// ProfileCreator.profileExternalAddressSize);
		// Map<InetAddress, int[]> nConnectionsInByInternalHost = new HashMap<InetAddress, int[]>(
		// ProfileCreator.profileInternalAddressSize);
		// Map<InetAddress, int[]> nConnectionsOutByInternalHost = new HashMap<InetAddress, int[]>(
		// ProfileCreator.profileInternalAddressSize);

		for(int i = 0; i < tmpIn.size(); i++) {// each profile snapshot (ingoing connections)
			Collection<Flow> flows = tmpIn.get(i);
			nConnectionsIn[i] = flows.size();
			for(Byte b : NetworkUtils.getKnownNetworkProtocols())
				nConnectionsInByProt.get(b)[i] = JnidsUtils.getNConnections(flows, b);
			// Map<InetAddress, Integer> connectionsInByHost = new HashMap<InetAddress, Integer>(
			// ProfileCreator.snapshotAddressSize);
			// Map<InetAddress, Integer> connectionsOutByHost = new HashMap<InetAddress, Integer>(
			// ProfileCreator.snapshotAddressSize);
			// for(Flow f : flows) {
			// CollectionUtils.addCountToMap(connectionsInByHost, f.orgAddress.getAddress());
			// CollectionUtils.addCountToMap(connectionsOutByHost, f.destAddress.getAddress());
			// }
			// for(Map.Entry<InetAddress, Integer> e : connectionsInByHost.entrySet())
			// CollectionUtils.addToMap(nConnectionsOutByExternalHost, e.getKey(), tmpIn.size(), i, e
			// .getValue());
			// for(Map.Entry<InetAddress, Integer> e : connectionsOutByHost.entrySet())
			// CollectionUtils.addToMap(nConnectionsInByInternalHost, e.getKey(), tmpIn.size(), i, e
			// .getValue());
		}
		for(int i = 0; i < tmpOut.size(); i++) {// each profile snapshot (outgoing connections)
			Collection<Flow> flows = tmpOut.get(i);
			nConnectionsOut[i] = flows.size();
			for(Byte b : NetworkUtils.getKnownNetworkProtocols())
				nConnectionsOutByProt.get(b)[i] = JnidsUtils.getNConnections(flows, b);
			// Map<InetAddress, Integer> connectionsInByHost = new HashMap<InetAddress, Integer>(
			// ProfileCreator.snapshotAddressSize);
			// Map<InetAddress, Integer> connectionsOutByHost = new HashMap<InetAddress, Integer>(
			// ProfileCreator.snapshotAddressSize);
			// for(Flow f : flows) {
			// CollectionUtils.addCountToMap(connectionsInByHost, f.orgAddress.getAddress());
			// CollectionUtils.addCountToMap(connectionsOutByHost, f.destAddress.getAddress());
			// }
			// for(Map.Entry<InetAddress, Integer> e : connectionsInByHost.entrySet())
			// CollectionUtils.addToMap(nConnectionsOutByInternalHost, e.getKey(), tmpOut.size(), i, e
			// .getValue());
			// for(Map.Entry<InetAddress, Integer> e : connectionsOutByHost.entrySet())
			// CollectionUtils.addToMap(nConnectionsInByExternalHost, e.getKey(), tmpOut.size(), i, e
			// .getValue());
		}

		PuntualStats s = new PuntualStats(initTimeUTC);
		s.add(StatType.getStatType(CONNECTIONS_IN), Stats.getStats(nConnectionsIn));
		s.add(StatType.getStatType(CONNECTIONS_OUT), Stats.getStats(nConnectionsOut));
		for(Map.Entry<Byte, int[]> e : nConnectionsInByProt.entrySet())
			s.add(new StatType(CONNECTIONS_IN_BY_PROTOCOL, e.getKey()), Stats.getStats(e.getValue()));
		for(Map.Entry<Byte, int[]> e : nConnectionsOutByProt.entrySet()) 
			s.add(new StatType(CONNECTIONS_OUT_BY_PROTOCOL, e.getKey()), Stats.getStats(e.getValue()));

		// adding most active hosts (internal, external, outgoing, ingoing)
		// addByHostStats(CONNECTIONS_FROM_EXTERNAL, nConnectionsInByExternalHost, s);
		// addByHostStats(CONNECTIONS_TO_EXTERNAL, nConnectionsOutByExternalHost, s);
		// addByHostStats(CONNECTIONS_TO_INTERNAL, nConnectionsInByInternalHost, s);
		// addByHostStats(CONNECTIONS_FROM_INTERNAL, nConnectionsOutByInternalHost, s);

		if (!s.isEmpty()) {
			int nIn = 0, nOut = 0;
			for(int element : nConnectionsIn)
				nIn += element;
			for(int element : nConnectionsOut)
				nOut += element;
			// System.err.println(getClass().getSimpleName() + "  " + nIn + "/" + nOut + " connections. " + s
			// + "   " + tmpIn + "   " + tmpOut);
		}
		return s;
	}

	private static void addByHostStats(StatType.Type type, Map<InetAddress, int[]> nConnections,
		PuntualStats ps) {
		if (nConnections.isEmpty())
			return;
		Map<InetAddress, Float> averageConnections = new HashMap<InetAddress, Float>((int) (nConnections
			.size() / 0.75) + 1);
		int l = nConnections.values().iterator().next().length;
		for(Map.Entry<InetAddress, int[]> e : nConnections.entrySet()) {
			float total = 0;
			for(int i = 0; i < l; i++)
				total += e.getValue()[i];
			averageConnections.put(e.getKey(), total / l);
		}
		Stats ss = Stats.getStats(averageConnections.values());
		ps.add(StatType.getStatType(type), ss);
		if (ss.standardDeviation == 0)
			return;
		// lower limit than the one at JnidsUtils.getMostActiveInternalHosts()
		float limit = ss.average + 1.1f * ss.standardDeviation;
		for(Map.Entry<InetAddress, Float> e : averageConnections.entrySet()) {
			int[] conns = nConnections.get(e.getKey());
			if (e.getValue() > limit && conns.length > ss.average)
				// SEE record below average connections (greater than a minimum like 1, of course)
				ps.add(new StatType(type, e.getKey()), Stats.getStats(conns));
		}
	}
}